 /*******************************************************************************
  * Copyright (c) 2000, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/

 package org.eclipse.ui.internal.console;

 import java.util.ArrayList ;
 import java.util.Iterator ;
 import java.util.regex.Matcher ;
 import java.util.regex.Pattern ;

 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.DocumentEvent;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IDocumentListener;
 import org.eclipse.ui.console.ConsolePlugin;
 import org.eclipse.ui.console.IPatternMatchListener;
 import org.eclipse.ui.console.PatternMatchEvent;
 import org.eclipse.ui.console.TextConsole;

 public class ConsolePatternMatcher implements IDocumentListener {

     private MatchJob fMatchJob = new MatchJob();

     /**
      * Collection of compiled pattern match listeners
      */
     private ArrayList fPatterns = new ArrayList ();

     private TextConsole fConsole;

     private boolean fFinalMatch;

     private boolean fScheduleFinal;

     public ConsolePatternMatcher(TextConsole console) {
         fConsole = console;
     }

     private class MatchJob extends Job {
         MatchJob() {
             super("Match Job"); //$NON-NLS-1$
 setSystem(true);
         }

         /*
          * (non-Javadoc)
          *
          * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
          */
         protected IStatus run(IProgressMonitor monitor) {
             IDocument doc = fConsole.getDocument();
             String text = null;
             int prevBaseOffset = -1;
             if (doc != null && !monitor.isCanceled()) {
                 int endOfSearch = doc.getLength();
                 int indexOfLastChar = endOfSearch;
                 if (indexOfLastChar > 0) {
                     indexOfLastChar--;
                 }
                 int lastLineToSearch = 0;
                 int offsetOfLastLineToSearch = 0;
                 try {
                     lastLineToSearch = doc.getLineOfOffset(indexOfLastChar);
                     offsetOfLastLineToSearch = doc.getLineOffset(lastLineToSearch);
                 } catch (BadLocationException e) {
                     // perhaps the buffer was re-set
 return Status.OK_STATUS;
                 }
                 for (int i = 0; i < fPatterns.size(); i++) {
                     if (monitor.isCanceled()) {
                         break;
                     }
                     CompiledPatternMatchListener notifier = (CompiledPatternMatchListener) fPatterns.get(i);
                     int baseOffset = notifier.end;
                     int lengthToSearch = endOfSearch - baseOffset;
                     if (lengthToSearch > 0) {
                         try {
                             if (prevBaseOffset != baseOffset) {
                                 // reuse the text string if possible
 text = doc.get(baseOffset, lengthToSearch);
                             }
                             Matcher reg = notifier.pattern.matcher(text);
                             Matcher quick = null;
                             if (notifier.qualifier != null) {
                                 quick = notifier.qualifier.matcher(text);
                             }
                             int startOfNextSearch = 0;
                             int endOfLastMatch = -1;
                             int lineOfLastMatch = -1;
                             while ((startOfNextSearch < lengthToSearch) && !monitor.isCanceled()) {
                                 if (quick != null) {
                                     if (quick.find(startOfNextSearch)) {
                                         // start searching on the beginning
 // of the line where the potential
 // match was found, or after the
 // last match on the same line
 int matchLine = doc.getLineOfOffset(baseOffset + quick.start());
                                         if (lineOfLastMatch == matchLine) {
                                             startOfNextSearch = endOfLastMatch;
                                         } else {
                                             startOfNextSearch = doc.getLineOffset(matchLine) - baseOffset;
                                         }
                                     } else {
                                         startOfNextSearch = lengthToSearch;
                                     }
                                 }
                                 if (startOfNextSearch < 0) {
                                     startOfNextSearch = 0;
                                 }
                                 if (startOfNextSearch < lengthToSearch) {
                                     if (reg.find(startOfNextSearch)) {
                                         endOfLastMatch = reg.end();
                                         lineOfLastMatch = doc.getLineOfOffset(baseOffset + endOfLastMatch - 1);
                                         int regStart = reg.start();
                                         IPatternMatchListener listener = notifier.listener;
                                         if (listener != null && !monitor.isCanceled()) {
                                             listener.matchFound(new PatternMatchEvent(fConsole, baseOffset + regStart, endOfLastMatch - regStart));
                                         }
                                         startOfNextSearch = endOfLastMatch;
                                     } else {
                                         startOfNextSearch = lengthToSearch;
                                     }
                                 }
                             }
                             // update start of next search to the last line
 // searched
 // or the end of the last match if it was on the
 // line that
 // was last searched
 if (lastLineToSearch == lineOfLastMatch) {
                                 notifier.end = baseOffset + endOfLastMatch;
                             } else {
                                 notifier.end = offsetOfLastLineToSearch;
                             }
                         } catch (BadLocationException e) {
                             ConsolePlugin.log(e);
                         }
                     }
                     prevBaseOffset = baseOffset;
                 }
             }

             if (fFinalMatch) {
                 disconnect();
                 fConsole.matcherFinished();
             } else if (fScheduleFinal) {
                 fFinalMatch = true;
                 schedule();
             }

             return Status.OK_STATUS;
         }

         public boolean belongsTo(Object family) {
             return family == fConsole;
         }
         
         
     }

     private class CompiledPatternMatchListener {
         Pattern pattern;

         Pattern qualifier;

         IPatternMatchListener listener;

         int end = 0;

         CompiledPatternMatchListener(Pattern pattern, Pattern qualifier, IPatternMatchListener matchListener) {
             this.pattern = pattern;
             this.listener = matchListener;
             this.qualifier = qualifier;
         }

         public void dispose() {
             listener.disconnect();
             pattern = null;
             qualifier = null;
             listener = null;
         }
     }

     /**
      * Adds the given pattern match listener to this console. The listener will
      * be connected and receive match notifications.
      *
      * @param matchListener
      * the pattern match listener to add
      */
     public void addPatternMatchListener(IPatternMatchListener matchListener) {
         synchronized (fPatterns) {
             
             // check for dups
 for (Iterator iter = fPatterns.iterator(); iter.hasNext();) {
                 CompiledPatternMatchListener element = (CompiledPatternMatchListener) iter.next();
                 if (element.listener == matchListener) {
                     return;
                 }
             }
             
             if (matchListener == null || matchListener.getPattern() == null) {
                 throw new IllegalArgumentException ("Pattern cannot be null"); //$NON-NLS-1$
 }

             Pattern pattern = Pattern.compile(matchListener.getPattern(), matchListener.getCompilerFlags());
             String qualifier = matchListener.getLineQualifier();
             Pattern qPattern = null;
             if (qualifier != null) {
                 qPattern = Pattern.compile(qualifier, matchListener.getCompilerFlags());
             }
             CompiledPatternMatchListener notifier = new CompiledPatternMatchListener(pattern, qPattern, matchListener);
             fPatterns.add(notifier);
             matchListener.connect(fConsole);
             fMatchJob.schedule();
         }
     }

     /**
      * Removes the given pattern match listener from this console. The listener
      * will be disconnected and will no longer receive match notifications.
      *
      * @param matchListener
      * the pattern match listener to remove.
      */
     public void removePatternMatchListener(IPatternMatchListener matchListener) {
         synchronized (fPatterns) {
             for (Iterator iter = fPatterns.iterator(); iter.hasNext();) {
                 CompiledPatternMatchListener element = (CompiledPatternMatchListener) iter.next();
                 if (element.listener == matchListener) {
                     iter.remove();
                     matchListener.disconnect();
                 }
             }
         }
     }

     public void disconnect() {
         fMatchJob.cancel();
         synchronized (fPatterns) {
             Iterator iterator = fPatterns.iterator();
             while (iterator.hasNext()) {
                 CompiledPatternMatchListener notifier = (CompiledPatternMatchListener) iterator.next();
                 notifier.dispose();
             }
             fPatterns.clear();
         }
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged(org.eclipse.jface.text.DocumentEvent)
      */
     public void documentAboutToBeChanged(DocumentEvent event) {
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse.jface.text.DocumentEvent)
      */
     public void documentChanged(DocumentEvent event) {
         if (event.fLength > 0) {
             synchronized (fPatterns) {
                 if (event.fDocument.getLength() == 0) {
                     // document has been cleared, reset match listeners
 Iterator iter = fPatterns.iterator();
                     while (iter.hasNext()) {
                         CompiledPatternMatchListener notifier = (CompiledPatternMatchListener) iter.next();
                         notifier.end = 0;
                     }
                 } else {
                     if (event.fOffset == 0) {
                         //document was trimmed
 Iterator iter = fPatterns.iterator();
                         while (iter.hasNext()) {
                             CompiledPatternMatchListener notifier = (CompiledPatternMatchListener) iter.next();
                             notifier.end = notifier.end > event.fLength ? notifier.end-event.fLength : 0;
                         }
                     }
                 }
             }
         }
         fMatchJob.schedule();
     }

     
     public void forceFinalMatching() {
         fScheduleFinal = true;
         fMatchJob.schedule();
     }

 }

